using System;
using System.Diagnostics;
using System.Collections.Generic;
using System.Threading;

public class SendDoubleBuffer
{
    private class DataBuffer
    {
        private byte[] m_buffer;
        private int m_rpos;
        private int m_wpos;

        public DataBuffer(int size)
        {
            m_buffer = new byte[size];
        }

        public byte[] GetBuffer()
        {
            return m_buffer;
        }

        public int GetPosition()
        {
            return m_rpos;
        }

        public bool IsReadableEmpty()
        {
            return m_rpos >= m_wpos;
        }

        public bool IsWritableFull()
        {
            return m_wpos >= m_buffer.Length;
        }
    
        public int GetReadableSize()
        {
            return m_wpos - m_rpos;
        }

        public int GetWritableSize()
        {
            return m_buffer.Length - m_wpos;
        }

        public void WriteData(byte[] data, int offset, int count)
        {
            Debug.Assert(m_wpos + count <= m_buffer.Length);
            Buffer.BlockCopy(data, offset, m_buffer, m_wpos, count);
            m_wpos += count;
        }

        public void RemoveData(int count)
        {
            Debug.Assert(m_rpos + count <= m_wpos);
            m_rpos += count;
        }

        public void Reset()
        {
            m_rpos = m_wpos = 0;
        }
    }

    private DataBuffer m_outBuffer;
    private DataBuffer m_inBuffer;
    private Queue<DataBuffer> m_inBuffers;
    private readonly int m_bufferSize;
    private long m_dataSize;

    public SendDoubleBuffer(int size = 65536)
    {
        m_bufferSize = size;
    }

    public long GetSendDataSize()
    {
        return m_dataSize;
    }

    public byte[] GetSendData(out int offset, out int size)
    {
        if (m_outBuffer != null && !m_outBuffer.IsReadableEmpty())
        {
            size = m_outBuffer.GetReadableSize();
            offset = m_outBuffer.GetPosition();
            return m_outBuffer.GetBuffer();
        }
        lock (this)
        {
            if (m_inBuffers == null || m_inBuffers.Count == 0)
            {
                Utils.Swap(ref m_inBuffer, ref m_outBuffer);
                if (m_inBuffer != null)
                {
                    m_inBuffer.Reset();
                }
            }
            else
            {
                m_outBuffer = m_inBuffers.Dequeue();
            }
        }
        if (m_outBuffer != null && !m_outBuffer.IsReadableEmpty())
        {
            size = m_outBuffer.GetReadableSize();
            offset = m_outBuffer.GetPosition();
            return m_outBuffer.GetBuffer();
        }
        size = offset = 0;
        return null;
    }

    public void RemoveSendData(int size)
    {
        Debug.Assert(m_outBuffer != null);
        if (m_outBuffer != null)
        {
            m_outBuffer.RemoveData(size);
            Interlocked.Add(ref m_dataSize, -size);
        }
    }

    public void WritePacket(NetPacket pck)
    {
        var size = pck.GetReadableSize();
        lock (this)
        {
            WritePacketHeader(pck.Opcode, NetPacket.HEADER_SIZE + size);
            WriteData(pck.GetBuffer(), pck.GetPosition(), size);
        }
    }

    public void WritePacket(NetPacket pck, byte[] data, int offset = 0, int count = -1)
    {
        count = count != -1 ? count : data.Length - offset;
        var size = pck.GetReadableSize();
        lock (this)
        {
            WritePacketHeader(pck.Opcode, NetPacket.HEADER_SIZE + size + count);
            WriteData(pck.GetBuffer(), pck.GetPosition(), size);
            WriteData(data, offset, count);
        }
    }

    public void WritePacket(NetPacket pck, NetPacket data)
    {
        var count = data.GetReadableSize();
        var size = pck.GetReadableSize();
        lock (this)
        {
            WritePacketHeader(pck.Opcode, (NetPacket.HEADER_SIZE << 1) + size + count);
            WriteData(pck.GetBuffer(), pck.GetPosition(), size);
            WritePacketHeader(data.Opcode, NetPacket.HEADER_SIZE + count);
            WriteData(data.GetBuffer(), data.GetPosition(), count);
        }
    }

    private void WritePacketHeader(int opcode, int size)
    {
        var header = new NetPacket(0, NetPacket.HEADER_SIZE);
        header.WritePacketHeader(opcode, size);
        WriteData(header.GetBuffer(), 0, header.GetSize());
    }

    private void WriteData(byte[] data, int offset, int count)
    {
        while (count > 0)
        {
            if (m_inBuffer != null && m_inBuffer.IsWritableFull())
            {
                if (m_inBuffers == null)
                {
                    m_inBuffers = new Queue<DataBuffer>(1);
                }
                m_inBuffers.Enqueue(m_inBuffer);
                m_inBuffer = null;
            }
            if (m_inBuffer == null)
            {
                m_inBuffer = new DataBuffer(m_bufferSize);
            }
            var size = Math.Min(m_inBuffer.GetWritableSize(), count);
            m_inBuffer.WriteData(data, offset, size);
            Interlocked.Add(ref m_dataSize, size);
            offset += size;
            count -= size;
        }
    }
}
